Skip to content

gh-156112: Load the ObjC runtime by name when find_library() cannot resolve it - #156113

Open
clementperon wants to merge 2 commits into
python:mainfrom
clementperon:ios-objc-fallback
Open

gh-156112: Load the ObjC runtime by name when find_library() cannot resolve it#156113
clementperon wants to merge 2 commits into
python:mainfrom
clementperon:ios-objc-fallback

Conversation

@clementperon

@clementperon clementperon commented Aug 20, 2026

Copy link
Copy Markdown
Contributor

_ios_support raises ImportError at import when ctypes.util.find_library("objc") returns None. That happens whenever the dyld shared cache cannot be queried: dyld_find() falls back to _dyld_shared_cache_contains_path(), which Modules/_ctypes/callproc.c guards with __builtin_available(macOS 11.0, iOS 14.0, tvOS 14.0, watchOS 7.0, *), so on iOS 13 it raises NotImplementedError and no path can be resolved for a library that exists only in the cache.

IPHONEOS_DEPLOYMENT_TARGET defaults to 13.0, so this sits inside the supported range, and it takes platform.platform() and platform.ios_ver() down with it.

dyld resolves a bare library name against the cache by itself, so this falls back to the name when find_library() comes up empty, and keeps reporting a real load failure as ImportError rather than letting OSError escape.

Verified on macOS, where the same cache mechanism applies — find_library("objc") names a path that is not a file, and a bare-name load works:

>>> util.find_library("objc"), os.path.isfile("/usr/lib/libobjc.dylib")
('/usr/lib/libobjc.dylib', False)
>>> cdll.LoadLibrary("libobjc.dylib")
<CDLL 'libobjc.dylib', handle ... at ...>

and with find_library patched to return None, the module loads the runtime and objc_getClass(b"NSProcessInfo") succeeds.

I do not have an iOS 13 device or simulator runtime available, so the import failure itself is derived from the source rather than reproduced on-device; the fallback is verified as above.

ctypes.util.find_library() resolves a system library through the dyld shared
cache, which requires _dyld_shared_cache_contains_path(). Modules/_ctypes/
callproc.c guards that symbol with __builtin_available(iOS 14.0), so on iOS 13 -
the oldest release CPython's iOS support targets - find_library() returns None
for any library that exists only in the cache, and importing _ios_support fails
outright rather than degrading.

dyld resolves a bare library name against the cache by itself, so use that when
find_library() comes up empty. A genuine load failure is still reported as
ImportError.
clementperon added a commit to clementperon/xbmc that referenced this pull request Aug 20, 2026
CPython appends -mios-version-min - the device flag - to every iOS build.
Against the simulator SDK the linker rejects the dylibs it resolves there, and
the first configure check that links one fails, taking the depends build with
it.

The local patch faked ac_sys_system=iOS by deleting CPython's host parsing,
because config.site.in pinned every package to the tree's darwin triplet. Python
now configures against its own iOS triplet, so CPython derives ac_sys_system,
the deployment target and the device/simulator split itself, and the 76 lines
that deleted that logic are gone.

What is left matches three patches under review upstream, so all three can be
dropped when python is next bumped:

  python/cpython#156110
  python/cpython#156116
  python/cpython#156113

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
clementperon added a commit to clementperon/xbmc that referenced this pull request Aug 20, 2026
CPython appends -mios-version-min - the device flag - to every iOS build.
Against the simulator SDK the linker rejects the dylibs it resolves there, and
the first configure check that links one fails, taking the depends build with
it.

The local patch faked ac_sys_system=iOS by deleting CPython's host parsing,
because config.site.in pinned every package to the tree's darwin triplet. Python
now configures against its own iOS triplet, so CPython derives ac_sys_system,
the deployment target and the device/simulator split itself, and the 76 lines
that deleted that logic are gone.

What is left matches three patches under review upstream, so all three can be
dropped when python is next bumped:

  python/cpython#156110
  python/cpython#156116
  python/cpython#156113

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
clementperon added a commit to clementperon/xbmc that referenced this pull request Aug 20, 2026
CPython appends -mios-version-min - the device flag - to every iOS build.
Against the simulator SDK the linker rejects the dylibs it resolves there, and
the first configure check that links one fails, taking the depends build with
it.

The local patch faked ac_sys_system=iOS by deleting CPython's host parsing,
because config.site.in pinned every package to the tree's darwin triplet. Python
now configures against its own iOS triplet, so CPython derives ac_sys_system,
the deployment target and the device/simulator split itself, and the 76 lines
that deleted that logic are gone.

What is left matches three patches under review upstream, so all three can be
dropped when python is next bumped:

  python/cpython#156110
  python/cpython#156116
  python/cpython#156113

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@freakboy3742

Copy link
Copy Markdown
Contributor

Is this a problem with an actual reproducible failure mode? If so, what is it?

In general, bug fixes are a lot more helpful when they're presented in the context of a reproducible problem. It is then possible to prove that the problem is resolved by applying the patch and testing again.

In this case, the proposed solution is that libobjc.dylib will be a useful fallback if find_library("objc") fails... is there any reason to believe that (a) this is a reasonable fallback on iOS, or (b) that this fallback resolves an actual reproducible problem that some actually has?

@clementperon

Copy link
Copy Markdown
Contributor Author

There is a reproducible case — I should have led with it.

Kodi hits this on iOS. Reported by @kambala-decapitator in xbmc/xbmc#28808, tested on iOS 12, 15 and 26 devices: inside an app sandbox the os.path.isfile() branch of dyld_find() can never succeed for /usr/lib/..., so resolution depends entirely on _dyld_shared_cache_contains_path(). That is gated by __builtin_available(macOS 11.0, iOS 14.0, tvOS 14.0, watchOS 7.0, *) in Modules/_ctypes/callproc.c, so below iOS 14 find_library() returns None for any system library and import _ios_support fails outright.

Kodi carries a downstream patch hardcoding /usr/lib/libobjc.dylib. This PR uses the bare name instead, which dyld resolves against the shared cache — the same lookup without the hardcoded path. Their testing notes the same thing: stat fails, dlopen succeeds.

On (a): confirmed on main by making the probe raise NotImplementedError, as _ctypes does when the symbol is missing:

find_library('objc') with the probe unavailable: None
unpatched _ios_support: ImportError: ObjC runtime library couldn't be loaded
patched   _ios_support: imported OK; objc_getClass(NSProcessInfo) -> True

iOS 12 is below the supported minimum, but iOS 13 is inside it and has the same gap.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants